// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/coinone/order_v2/parser.h"

#include <sstream>

#include <boost/algorithm/string.hpp>

#include "coin/proto/coin_query.pb.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::coinone::order_v2 {

auto CoinoneParser::native_currencies() const -> const CurrencyPairs& {
  static CurrencyPairs currency_pairs = [&]() -> auto {
    CurrencyPairs tmp;
    auto currency_set = symcache_->GetCurrencies();
    for (auto& currency : currency_set) {
      const auto& native_currency = currency->native_currency();
      tmp.emplace(std::make_pair(boost::to_lower_copy(native_currency), native_currency));
    }
    return tmp;
  }();
  return currency_pairs;
}

int64_t CoinoneParser::GetTimestamp(const std::string& time_str) const {
  return stoll(time_str) * 1e9;
}

int CoinoneParser::GetErrorCode(const JsonValue& data) const {
  if (data.HasMember("errorCode")) {
    return std::stoi(data["errorCode"].GetString());
  }

  if (data.HasMember("error_code")) {
    return std::stoi(data["error_code"].GetString());
  }

  return -1;
}

std::string CoinoneParser::GetErrorMsg(const JsonValue& data) const {
  if (data.HasMember("errorMsg")) {
    return data["errorMsg"].GetString();
  }

  if (data.HasMember("error_msg")) {
    return data["error_msg"].GetString();
  }

  return "Unknown error msg, no errorMsg or error_msg field in response";
}

bool CoinoneParser::IsCoinoneErrMessage(const JsonValue& data) const {
  std::string result = "";
  if (data.HasMember("result")) {
    result = data["result"].GetString();
  }
  int error_code = GetErrorCode(data);
  return !(result == "success" && error_code == 0);
}

int64_t CoinoneParser::GetProcOrderId(const JsonValue& data) const { return 0; }

auto CoinoneParser::GetOrderSide(const std::string& side) const -> OrderSide {
  if (side == "bid") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "ask") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

auto CoinoneParser::GetOrderType(const std::string& type) const -> OrderType {
  return coin::proto::OrderType::LIMIT_ORDER;
}

void CoinoneParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[CoinoneSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << "AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[CoinoneSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() != 200) {
    LOG(ERROR) << "[CoinoneSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << "status code: " << http_context->res.result_int()
               << ", payload: " << http_context->res.body().data();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[CoinoneSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << "Parse Error\n"
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (json.IsObject() && IsCoinoneErrMessage(json)) {
    LOG(ERROR) << "[CoinoneSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << JsonToString(json);
    int error_code = GetErrorCode(json);
    switch (error_code) {
      case 103:
        resp->set_error_code(coin::proto::OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND);
        break;
      case 104:
        resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
        break;
      default:
        resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
        break;
    }
    resp->set_exchange_error_code(std::to_string(error_code));
    resp->set_exchange_error_msg(GetErrorMsg(json));
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_exchange_order_id(json["orderId"].GetString());
}

void CoinoneParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[CoinoneCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << "AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[CoinoneCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() != 200) {
    LOG(ERROR) << "[CoinoneCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << "status code: " << http_context->res.result_int()
               << ", payload: " << http_context->res.body().data();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[CoinoneCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << "Parse Error\n"
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (json.IsObject() && IsCoinoneErrMessage(json)) {
    LOG(ERROR) << "[CoinoneCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << JsonToString(json);
    int error_code = GetErrorCode(json);
    switch (error_code) {
      case 103:
        resp->set_error_code(coin::proto::OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND);
        break;
      case 104:
        resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
        break;
      case 116:
        resp->set_error_code(coin::proto::OrderErrorCode::ORDER_FILLED_ALREADY);
        break;
      default:
        resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
        break;
    }
    resp->set_exchange_error_code(std::to_string(error_code));
    resp->set_exchange_error_msg(GetErrorMsg(json));
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

void CoinoneParser::ParseRestLimitOrder(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  /*
    {
    "index": "0",
    "orderId": "6b2dcb21-1e4d-11e9-9ec7-00e04c3600d7",
    "timestamp": "1599636689",
    "price": "284.3",
    "qty": "200.0",
    "type": "bid",
    "feeRate": "0.0"
  }
  */

  product_order_element->set_market_type(MarketType::Spot);
  product_order_element->set_exchange_type(ExchangeType::Coinone);
  product_order_element->set_api_version("v2");
  product_order_element->set_exchange_order_id(data["orderId"].GetString());
  product_order_element->set_symbol(current_native_symbol_);
  product_order_element->set_price(stod(data["price"].GetString()));
  product_order_element->set_qty(stod(data["qty"].GetString()));
  product_order_element->set_proc_order_id(GetProcOrderId(data));
  product_order_element->set_side(GetOrderSide(data["type"].GetString()));
  product_order_element->set_order_type(GetOrderType("limit"));
  product_order_element->set_timestamp(msg_->timestamp);
  product_order_element->set_is_live(true);
}

void CoinoneParser::ParseRestOrdersInfo() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestOrdersInfo] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsCoinoneErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrdersInfo] failed. " << JsonToString(data);
    return;
  }

  ProductOrderBundle& pob = *result_.mutable_order_list();
  pob.Clear();
  bool has_open_order = false;
  if (data.HasMember("limitOrders")) {
    // limit orders
    for (const auto& info : data["limitOrders"].GetArray()) {
      auto order = pob.add_each_order();
      order->Clear();
      ParseRestLimitOrder(info, order);
      has_open_order = true;
    }
  }
  if (data.HasMember("info")) {
    // order_info
  }
  if (has_open_order) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void CoinoneParser::ParseRestFillInfo(
    const JsonValue& data,
    ProductFillElement* product_fill_element) {
  /*
  {
    "result": "success",
    "errorCode": "0",
    "completeOrders": [
      {
        "orderId": "6bde4964-1e4d-11e9-9ec7-00e04c3600d7",
        "type": "ask",
        "price": "287.6",
        "qty": "9.9989",
        "fee": "0.2875",
        "feeRate": "0.00010",
        "timestamp": "1599820643"
      },
      {
        "orderId": "6bde47ed-1e4d-11e9-9ec7-00e04c3600d7",
        "type": "ask",
        "price": "287.5",
        "qty": "6.4",
        "fee": "0.184",
        "feeRate": "0.00010",
        "timestamp": "1599820640"
      },
      {
        "orderId": "6bde47ed-1e4d-11e9-9ec7-00e04c3600d7",
        "type": "ask",
        "price": "287.6",
        "qty": "3.6",
        "fee": "0.1035",
        "feeRate": "0.00010",
        "timestamp": "1599820640"
      },
      {
        "orderId": "6bde47ed-1e4d-11e9-9ec7-00e04c3600d7",
        "type": "ask",
        "price": "287.6",
        "qty": "10.0",
        "fee": "0.2876",
        "feeRate": "0.00010",
        "timestamp": "1599820640"
      }
    }
  }
  */

  const std::string order_id = data["orderId"].GetString();
  const std::string timestamp = data["timestamp"].GetString();
  const std::string price = data["price"].GetString();
  const std::string qty = data["qty"].GetString();
  product_fill_element->set_symbol(current_native_symbol_);
  product_fill_element->set_fill_timestamp(msg_->timestamp);
  product_fill_element->set_exchange_order_id(order_id);
  product_fill_element->set_price(stod(price));
  product_fill_element->set_qty(stod(qty));
  product_fill_element->set_side(GetOrderSide(data["type"].GetString()));
  std::stringstream ss;
  ss << order_id << "_" << timestamp << "_" << price << "_" << qty;
  product_fill_element->set_fill_id(ss.str());
  product_fill_element->set_fee(stod(data["fee"].GetString()));
}

void CoinoneParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto CoinoneParser::ParseBalance(const JsonValue& data) -> AccountBalance {
  /*
  {
    "result": "success",
    "errorCode": "0",
    "normalWallets": [],
    "nest": {
      "avail": "0.0",
      "balance": "0.0"
    },
    "bal": {
      "avail": "0.0",
      "balance": "0.0"
    },
    "dot": {
      "avail": "0.0",
      "balance": "0.0"
    }
  }
  */
  AccountBalance acc_balance;
  if (data.IsObject() && IsCoinoneErrMessage(data)) {
    LOG(ERROR) << "[ParseBalance] failed. " << JsonToString(data);
    return acc_balance;
  }
  acc_balance.set_exchange("Coinone");
  acc_balance.set_market_type("Spot");
  for (auto& curr : native_currencies()) {
    const auto& currency_lower = curr.first;
    const auto& currency_upper = curr.second;
    if (!data.HasMember(currency_lower.c_str())) {
      continue;
    }
    const auto& curr_obj = data[currency_lower.c_str()].GetObject();
    auto currency = symcache_->GetCurrencyByNative(currency_upper);
    auto currency_balance = acc_balance.add_each_balance();
    currency_balance->Clear();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    currency_balance->set_available(stod(curr_obj["avail"].GetString()));
    currency_balance->set_total(stod(curr_obj["balance"].GetString()));
    currency_balance->set_hold(currency_balance->total() - currency_balance->available());
  }

  return acc_balance;
}

void CoinoneParser::ParseRestFillInfoList() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsCoinoneErrMessage(data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed. " << JsonToString(data);
    return;
  }
  if (!data.HasMember("completeOrders")) {
    return;
  }
  ProductFillBundle& pfb = *result_.mutable_fill_list();
  pfb.Clear();
  bool has_complete_order = false;
  for (const auto& info : data["completeOrders"].GetArray()) {
    auto fill = pfb.add_each_fill();
    fill->Clear();
    ParseRestFillInfo(info, fill);
    has_complete_order = true;
  }
  if (has_complete_order) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void CoinoneParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  // websocket unsupported
}

auto CoinoneParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  current_native_symbol_ = msg.http_context->extra_info;
  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::coinone::order_v2
